// Copyright 2009 sbva project
//
// This file is part of sbva,
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.googlecode.sbva;

import com.googlecode.sbva.PropertyAndErrorMessages.ErrorMessage;
import com.googlecode.sbva.PropertyAndErrorMessages.ErrorMessages;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import javax.validation.ConstraintViolation;
import javax.validation.Path;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.action.ActionErrors;
import org.apache.struts.action.ActionMessage;

/**
 * This is aa abstract base class useful for implementing jsr-303 implementations in
 * struts orfor unit tests of you sbva jsr-303 code.  Specifically:
 * <ul>
 * <li> DefaultStrutsBeanValidationAdapter - is used in your ActionForm code.
 * <li> ResourceBundleBeanValidationAdapter - is used in your unit tests to validate
 *          your validation logic.
 * </ul>
 *
 * The jsr-303 annotation attribute <code>message</code> is used by this class
 * using a specialized format that sbva uses to associate ban properties, 
 * message kets, and message arguments.  This url format will now be
 * described using this example.
 * <p><code>
 * struts://<i>message.resource.key</i>/?0=<i>message.param.0</i>&1=<i>message.param.1</i>&...
 * <p></code>
 * such as
 * <p><code>
 * struts://input.required/?0=create.user.address.address
 * <p></code>
 * <ul>
 * <li> url scheme : struts - identifies this message as a sbva formatted error message
 * <li> url domain : input.required - a message resource key that was specified struts-config.xml message-resources.
 * <li> url query parameters : 0=create.user.address.address - a message resource parameter of key that was specified struts-config.xml message-resources.  These paraameters are optional and processed in order.
 * </ul>

 *
 */
public abstract class StrutsBeanValidationAdapter<T> {

   private static Log log = LogFactory.getLog(StrutsBeanValidationAdapter.class);
   private static Pattern substitutionVariablePattern = Pattern.compile("\\{([A-Za-z]+)\\}");
   private static Validator validator;
   private static final String SCHEME_STRUTS = "struts";
   private static final String keyValueSeparator = "=";

   static {
      try {

         ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
         validator = factory.getValidator();

      } catch (Exception e) {
         e.printStackTrace();
         throw new RuntimeException(e);
      }
   }

   private String queryStringSeparator = "&";

   /**
    *
    * Sets the query string pair separator to the semicolon.
    *
    * http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2
    *
    */
   public void setSemicolonQueryStringSeparator() { queryStringSeparator = ";"; }
   
   /**
    *
    * Sets the query string pair separator to the ampersand.
    *
    */
   public void setAmpersandQueryStringSeparator() { queryStringSeparator = "&"; }

   protected ActionErrors map2ActionErrors(PropertyAndErrorMessages errorInfos) {

      ActionErrors errors = new ActionErrors();

      return map2ActionErrors(errors, errorInfos);

   }

   /**
    *
    * Allows the "generic" messages obtained from
    * {#link #beanValidationErrorsToActionErrors(T, LookupProperty, PropertyAndErrorMessages, Class<T>)}
    *
    *
    */
   protected ActionErrors map2ActionErrors(ActionErrors errors, PropertyAndErrorMessages errorInfos) {

      Set<String> inputProperties = errorInfos.keySet();

      for (String inputProperty : inputProperties) {

         ErrorMessages errorMessages = errorInfos.get(inputProperty);

         for (ErrorMessage errorMessage : errorMessages) {

            if (errorMessage.getErrorParameters() != null) {
               errors.add(inputProperty, new ActionMessage(errorMessage.getErrorKey(), errorMessage.getErrorParameters()));
            } else {
               errors.add(inputProperty, new ActionMessage(errorMessage.getErrorKey()));
            }

         }

      }

      return errors;

   }

   protected PropertyAndErrorMessages beanValidationErrorsToActionErrors(T model, LookupProperty lookup, Class<T>... groups) {

      PropertyAndErrorMessages errors = new PropertyAndErrorMessages();

      beanValidationErrorsToActionErrors(model, lookup, errors, groups);

      return errors;
   }

   /**
    * This method performs the adapting between jsr-303 bean validation and
    * Struts 1 validation.  It shold be invoked in the ActionForm validate
    * method like thid.
    *
    * <code>
    * public class UserForm extends ActionForm {
    *   private static StrutsBeanValidationAdapter beanValidationToStrutsConverter =
    *     new DefaultStrutsBeanValidationAdapter();
    * .
    * .
    * .
    *   @Override
    *   public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {
    *     return beanValidationToStrutsConverter.beanValidationErrorsToActionErrors(this, request);
    *   }
    * .
    * .
    * .
    * }
    * </code>
    *
    * This will invoke {@link #mapPropertyToFormInput(java.lang.String)  } for each error encounterd.
    *
    * @param model - the object containg the JSR-303 bean validations to be validated.  This is usually an ActionForm.
    * @param lookup - the current HttpServletRequest parameters
    * @param errors - the ActionErrors to add the JSR-303 BeanValidation errors to for display using <code>&gt;html:errors key="..."/&lt;
    * @param groups - the groups that define what validations should occur.
    *
    */
   protected void beanValidationErrorsToActionErrors(T model, LookupProperty lookup, PropertyAndErrorMessages errors, Class<T>... groups) {

      Set<ConstraintViolation<T>> constraintViolations = null;

      constraintViolations = validator.validate(model, groups);

      for (ConstraintViolation<T> constraintViolation : constraintViolations) {
         Path path = constraintViolation.getPropertyPath();

         String propertyName = path.toString();
         propertyName = mapPropertyToFormInput(propertyName);

         // whis is this used -- only show one error per message?
         //if (propertyName == null) {
         //   continue;
         //}

         String message = constraintViolation.getMessage();

         try {
            URI messageUri = URI.create(message);

            if (SCHEME_STRUTS.equals(messageUri.getScheme())) {
               String key = messageUri.getHost();
               String queryString = messageUri.getQuery();
               if (queryString != null) {
                  List<String> params = new ArrayList<String>();
                  String[] values = queryString.split(queryStringSeparator);
                  for (String keyAndValueString : values) {
                     String[] keyAndValue = keyAndValueString.split(keyValueSeparator);
                     String value = keyAndValue[1];

                     String tmpValue = lookup.lookup(value);
                     if (tmpValue != null) {
                        value = tmpValue;
                     }
                     params.add(value);
                  }
                  ErrorMessages errorMessages = errors.get(propertyName);
                  if (errorMessages == null) {
                     errorMessages = new ErrorMessages(key, params.toArray(new String[params.size()]));
                  } else {
                     errorMessages.add(new ErrorMessage(key, params.toArray(new String[params.size()])));
                  }

                  errors.put(propertyName, errorMessages);
               } else {
                  ErrorMessages errorMessages = errors.get(propertyName);
                  if (errorMessages == null) {
                     errorMessages = new ErrorMessages(key);
                  } else {
                     errorMessages.add(new ErrorMessage(key));
                  }
                  errors.put(propertyName, errorMessages);
               }

            }

         } catch (Exception e) {
            errors.add(propertyName, message);
         }

      }

   }

   /**
    * This allows error properties to me mapped to more appropriate
    * names for display using the html:errors jsp tag.  This may be needed
    * because the property name processed by the jsr-303 bean validation is
    * different than the propery used in the html input frm used for struts
    * error reporting.
    * <p>
    * If a null is returned by this method, that property will be skipped.
    * <p>
    *
    * @param paramString
    * @return
    */
   protected abstract String mapPropertyToFormInput(String paramString);
}
